# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: leon

import json
import logging
from typing import List, Optional, Union

from coin.base.config import Config
from coin.base.param_util import to_list
from coin.exchange.base.order_gateway import (OrderGatewayBase,
                                              Order,
                                              OrderSide,
                                              OrderType,
                                              OrderGatewayStatus,
                                              BaseOrderGatewayInfoMutator)

from coin.exchange.bittrex_v1.kr_rest.currency import BittrexCurrency
from coin.exchange.bittrex_v1.kr_rest.product import BittrexProduct
from coin.exchange.bittrex_v1.rest.http_client import BittrexHttpClient
from coin.exchange.bittrex_v1.rest.order import BittrexRestOrder
from coin.exchange.bittrex_v1.rest.private import BittrexPrivateQueryer
import coin.flow.subscriber as flow_subscriber
from coin.proto.coin_query_pb2 import (AccountBalance, CurrencyBalance)


class BittrexGatewayInfo(BaseOrderGatewayInfoMutator):
  CurrencyType = BittrexCurrency
  ProductType = BittrexProduct
  BalanceType = float
  PositionType = None

  def __init__(self, currencies: List[BittrexCurrency], products: List[BittrexProduct]):
    super().__init__(currencies, products)

  def update_bittrex_balance(self, currency, currency_balance: CurrencyBalance):
    self.set_balance(currency, currency_balance.total, currency_balance)

  def balance_as_proto(self):
    return AccountBalance(exchange='Bittrex', each_balance=self.get_notNone_balances_internal())


class BittrexOrderGateway(OrderGatewayBase):
  OGInfoType = BittrexGatewayInfo

  def __init__(self,
               currencies: List[BittrexCurrency],
               products: Union[List[BittrexProduct], BittrexProduct],
               config: Config):
    super().__init__()

    self._config = config
    self._og_info = BittrexGatewayInfo(currencies, to_list(products))
    self._working_orders = {}
    self._order = None
    self._next_order_id = 1000000

    self._balance_ready = False
    self._orders_ready = False

  def start(self, queue, ioloop, flow_sub=None):
    bittrex_client = BittrexHttpClient(self._config)
    self._order = BittrexRestOrder(bittrex_client, ioloop)
    self._order.handle_order = self._handle_order
    self._ioloop = ioloop
    self._private_queryer = BittrexPrivateQueryer(
        queue,
        1,  # TODO(inkyu): WORKER_ID
        self._og_info.get_products(),
        self._working_orders)
    self._private_queryer.start(bittrex_client, ioloop)

    self._flow_sub = flow_sub or flow_subscriber.from_queue(queue)
    self._flow_sub.subscribe('bittrex_rest_private_1', self.on_rest_private_msg)

  def stop(self):
    self._flow_sub.unsubscribe('bittrex_rest_private_1', None)

  def _check_ready(self):
    if self._balance_ready and self._orders_ready:
      self._og_ready.set_status(OrderGatewayStatus.READY)

  def _find_order(self, server_order_id) -> Union[Order, None]:
    for order in self._working_orders.values():
      if ('server_order_id' in order.internal
          and order.internal['server_order_id'] == server_order_id):
        return order
    return None

  def _handle_rest_filled_orders(self, message):
    # Lock
    self._balance_ready = False

    if message['success'] != True:
      logging.error('success is false: %s' % message)
      return

    for entry in message['result']:
      order_id = str(entry['OrderUuid'])
      native_symbol = entry['Exchange']
      product = BittrexProduct.FromStr(native_symbol)
      price = float(entry['Limit'])
      qty = float(entry['Quantity'])
      order_side = {'LIMIT_BUY': OrderSide.BUY, 'LIMIT_SELL': OrderSide.SELL}[entry['OrderType']]
      remaining = float(entry['QuantityRemaining'])
      fill_qty = float(entry['Quantity'])
      fill_amount = float(entry['Price'])
      filled_at = entry['TimeStamp']

      order = self._find_order(order_id)
      if order:
        assert order.product == product
        assert float(order.price) == price, (order.price, price)
        assert float(order.qty) == qty, (order.qty, qty)
        assert order.order_side == order_side, (order.order_side, order_side)
        # TODO(inkyu): Implement
        if remaining == 0:
          logging.info('Bittrex: FILL %s %d @ %.8f' % (order_side, fill_qty, fill_amount))
          del self._working_orders[order.order_id]
      else:
        # Doesn't matter, already removed.
        pass

    self._orders_ready = True
    self._check_ready()

  def _handle_rest_order_status(self, message):
    # Lock
    self._balance_ready = False

    if message['success'] != True:
      logging.error('success is false: %s' % message)
      return

    result = message['result']
    order_id = str(result['OrderUuid'])
    native_symbol = result['Exchange']
    product = BittrexProduct.FromStr(native_symbol)
    price = float(result['Limit'])
    qty = float(result['Quantity'])
    order_side = {'LIMIT_BUY': OrderSide.BUY, 'LIMIT_SELL': OrderSide.SELL}[result['Type']]
    remaining = float(result['QuantityRemaining'])
    is_open = result['IsOpen']
    closed = result['Closed']

    order = self._find_order(order_id)
    if order:
      assert order.product == product
      assert float(order.price) == price, (order.price, price)
      assert float(order.qty) == qty, (order.qty, qty)
      assert order.order_side == order_side, (order.order_side, order_side)
      if not is_open and remaining == 0:
        fill_qty = float(result['Quantity'])
        fill_amount = float(result['Price'])
        logging.info('Bittrex: FILLED %s %s %.8f @ %.8f' %
                     (order_side, product.symbol, fill_qty, fill_amount))
        del self._working_orders[order.order_id]
    else:
      # TODO(leon): Get canceled?
      logging.info('Bittrex: order status updated but not in working list, canceled? %s %.8f @ %.8f'
                   % (order_side, qty, remaining))

    self._orders_ready = True
    self._check_ready()

  def _handle_rest_open_orders(self, message):
    # Lock
    self._balance_ready = False

    if message['success'] != True:
      logging.error('success is false: %s' % message)
      return

    logging.debug('Bittrex open orders: %s' % message)
    order_ids_to_remove = []
    for entry in message['result']:
      order_id = str(entry['OrderUuid'])
      native_symbol = entry['Exchange']
      product = BittrexProduct.FromStr(native_symbol)
      price = float(entry['Limit'])
      qty = float(entry['Quantity'])
      order_side = {'LIMIT_BUY': OrderSide.BUY, 'LIMIT_SELL': OrderSide.SELL}[entry['OrderType']]
      remaining = float(entry['QuantityRemaining'])

      order = self._find_order(order_id)
      if not order:
        if remaining > 0:
          order_ids_to_remove.append(order_id)

    # Cancel unknown orders
    if len(order_ids_to_remove) > 0:
      self._cancel_by_server_order_ids(order_ids_to_remove[:50])

    self._orders_ready = True
    self._check_ready()

  def _handle_rest_balance(self, message):
    if message['success'] != True:
      logging.error('success is false: %s' % message)
      return
    logging.debug('Bittrex balance: %s' % message)
    for entry in message['result']:
      currency = BittrexCurrency.FromStr(entry['Currency'])
      available = float(entry['Available'])
      hold = float(entry['Pending'])
      assert available >= 0.0
      assert hold >= 0.0
      total = available + hold
      currency_balance = CurrencyBalance(currency=currency.currency,
                                         currency_native=currency.native_currency,
                                         total=total,
                                         available=available,
                                         hold=hold)
      self._og_info.update_bittrex_balance(currency, currency_balance)
    self._balance_ready = True
    self._check_ready()

  def on_rest_private_msg(self, record, queue_data, topic_data):
    message = record.data

    query = message['query']
    if query == 'order':
      self._handle_rest_order_status(message['data'])
    elif query == 'open_orders':
      self._handle_rest_open_orders(message['data'])
    elif query == 'balance':
      self._handle_rest_balance(message['data'])
    else:
      raise ValueError('Unknown query: %s' % query)

  def get_working_order(self):
    return self._working_orders.values()

  def _handle_order(self, order_id, response):
    if not response:
      logging.error('Bittrex place order request failed: %s' % order_id)
      del self._working_orders[order_id]
      return
    message = json.loads(response.body)
    if message['success']:
      order = self._working_orders[order_id]
      order.accepted = True
      order.internal['server_order_id'] = message['result']['uuid']
      logging.debug(order)
    else:
      logging.error('Bittrex place order %s failed: %s.' % (order_id, message['message']))
      del self._working_orders[order_id]

  def submit_impl(
      self,
      product: BittrexProduct,
      price: str,  # TODO(leon): precision
      qty: str,
      order_side: OrderSide,
      order_type: OrderType = OrderType.LIMIT,  # LIMIT
      order_id: Optional[str] = None,
      tag=None):
    assert isinstance(product, BittrexProduct)
    assert self.is_ready()
    if order_side == OrderSide.BUY:
      side = 'buy-limit'
    elif order_side == OrderSide.SELL:
      side = 'sell-limit'
    else:
      raise ValueError('Unknown side: %s' % order_side)
    if order_type != OrderType.LIMIT:
      raise NotImplementedError()

    order_id = order_id or ('pl_%d' % self._next_order_id)
    order = Order(order_id=order_id,
                  product=product,
                  price=price,
                  qty=qty,
                  order_side=order_side,
                  order_type=order_type,
                  tag=None,
                  accepted=False,
                  cancel_sent=False,
                  internal={})
    self._next_order_id += 1
    self._working_orders[order_id] = order

    self._balance_ready = False
    logging.info('Bittrex place order: %s, %s, %s, %s, %s' %
                 (order_id, product.native_symbol, side, price, qty))
    return self._order.place_order(order_id, product.native_symbol, side, price, qty)

  def has_pending_order(self, product):
    for order in self._working_orders.values():
      if order.product == product and 'server_order_id' not in order.internal:
        return True
    return False

  def cancel_multiple_impl(self, order_ids, *, ignore_error=False):
    server_order_ids = []
    for order_id in order_ids:
      try:
        order = self._working_orders[order_id]
        server_order_id = order.internal['server_order_id']
        server_order_ids.append(server_order_id)
        # TODO(inkyu): Handle Failure
        del self._working_orders[order_id]
      except KeyError:
        logging.error('order:%s is not accepted yet.' % order_id)
        if not ignore_error:
          raise ValueError('order:%s is not accepted yet.' % order_id)

    self._balance_ready = False
    return self._cancel_by_server_order_ids(server_order_ids)

  def _cancel_by_server_order_ids(self, server_order_ids: list):
    logging.debug('Bittrex cancel orders: %s.' % str(server_order_ids))
    return self._order.cancel_orders(server_order_ids)

  def cancel_all_impl(self, *, ignore_error=False):
    raise NotImplementedError()

  def order_ids_by_product(self, product):
    res = []
    for order in self._working_orders.values():
      if order.product == product:
        res.append(order.order_id)
    return res
